#!/usr/bin/env python
#
# Copyright (C) 2007 Chris Chou <m2chrischou@gmail.com>. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 
# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# $Id: svn2cvs,v 1.6 2007/10/30 16:19:48 chris Exp $


import getopt
import os
import sys
import time
import popen2
from xml.dom import minidom
from xml.parsers.expat import ExpatError

_verbose = False

def escape (text):
	text = text.replace ('\\', '\\\\')
	text = text.replace ('|', '\|')
	text = text.replace ('&', '\&')
	text = text.replace (';', '\;')
	text = text.replace ('<', '\<')
	text = text.replace ('>', '\>')
	text = text.replace ('(', '\(')
	text = text.replace (')', '\)')
	text = text.replace ('$', '\$')
	text = text.replace ('`', '\`')
	text = text.replace ('"', '\\"')
	text = text.replace ("'", "\\'")
	text = text.replace (' ', '\ ')
	text = text.replace ('*', '\*')
	text = text.replace ('?', '\?')
	text = text.replace ('[', '\[')
	text = text.replace ('#', '\#')
	text = text.replace ('~', '\~')
	text = text.replace ('=', '\=')
	text = text.replace ('%', '\%')
	text = text.replace (':', '\:')
	return text

def parseRevision (svnrepos) :
	xml = "".join(os.popen ("svn log --non-interactive --xml -v -r1:HEAD %s" % escape(svnrepos)).readlines ());

	xmldoc = None
	try :
		xmldoc = minidom.parseString (xml);
	except ExpatError :
		xmldoc = None

	if not xmldoc :
		return xmldoc

	log = []
	for entryNode in xmldoc.documentElement.childNodes :
		if entryNode.nodeType != entryNode.ELEMENT_NODE :
			continue

		entry = {'author':'', 'date':'', 'msg':''}

		log.append (entry)
		entry['revision'] = entryNode.attributes['revision'].value
		entry['paths'] = []

		for itemNode in entryNode.childNodes :
			if itemNode.nodeType != itemNode.ELEMENT_NODE :
				continue
			
			if itemNode.tagName not in ('author', 'date', 'msg', 'paths') :
				print 'Warning: unrecognized entry item: %s in revision %s.' % (itemNode.tagName, entry['revision'])

			if itemNode.tagName != 'paths' :
				if itemNode.childNodes.length :
					entry[itemNode.tagName] = itemNode.childNodes[0].nodeValue
			else :
				for pathNode in itemNode.childNodes :
					if pathNode.nodeType != pathNode.ELEMENT_NODE :
						continue

					path = {}
					entry['paths'].append (path)

					for attrKey in pathNode.attributes.keys ():
						path[attrKey] = pathNode.attributes[attrKey].value

					path['value'] = ''
					if pathNode.childNodes.length:
						path['value'] = pathNode.childNodes[0].nodeValue.lstrip('/')

	xmldoc = None

	return log


# Determine whether a path can be omitted.
def canOmit (path):
	basename = os.path.basename (path);
	if basename in ('CVS', '.svn'):
		return True

	if path.find ('/CVS/') >= 0 or path.find ('/.svn/') >=0 :
		return True

	if path.startswith ('CVS/') or path.startswith ('.svn/'):
		return True

	return False

# Executes a command
def execute (cmd, verbose = True):
	if verbose or _verbose:
		print cmd

	p = popen2.Popen4 (cmd)
	p.tochild.close ()
	if verbose or _verbose:
		for l in p.fromchild:
			print l.rstrip ('\r\n')
	else:
		for l in p.fromchild:
			pass

	return p.wait ()


# Executes CVS 'checkout' command
def cvsCheckout (localdir, cvsroot, cvsmodule, verbose = False):
	if len (cvsroot):
		cvscmd = 'cvs -d %s co -d %s %s' % (escape(cvsroot), escape(localdir), escape(cvsmodule))
	else:
		cvscmd = 'cd %s && cvs co -d %s %s' % (escape (os.path.dirname (localdir)), escape(os.path.basename (localdir)), escape (cvsmodule))

	if execute (cvscmd, verbose):
		print 'Error: Failed to checkout module %s.' % cvsmodule
		return False

	print '[Checkouted] %s into %s.' % (cvsmodule, localdir)
	return True


# Executes CVS 'commit' command
def cvsCommit (path, msg, verbose = False):
	cvscmd = 'cvs -q ci -m "%s" %s' % (msg, escape (path))

	if execute (cvscmd, verbose):
		return False

	print '[Committed] .'

	return True


# Executes CVS 'add' command
def cvsAdd (path, verbose = False):
	#basename = os.path.basename (path)
	#if basename == '.svn' or basename == 'CVS':
	#	return True
	if canOmit (path):
		return True

	cvscmd = 'cvs add %s' % escape (path)

	binary = False
	directory = False
	filetype = 'application/x-not-regular-file'
	if not os.path.isdir (path):
		filetype = os.popen ('file -bi %s' % escape (path)).readline ().strip()
		if filetype:
			pre = filetype.split('/')[0]
			if not (pre == 'text' or filetype in ('image/svg+xml', 'application/x-empty')):
				cvscmd = 'cvs add -kb %s' % escape (path)
				binary = True
			else:
				pass
	else:
		directory = True

	if execute (cvscmd, verbose):
		print path
		return False

	if directory:
		print '[Added directory] %s.' % path
	elif binary:
		print '[Added binary] %s (%s).' % (path, filetype)
	else:
		print '[Added] %s (%s).' % (path, filetype)

	return True

def cvsAddRecursively (path, verbose = False):
	#basename = os.path.basename (path)
	#if basename == '.svn' or basename == 'CVS':
	if canOmit (path):
		return True

	if not cvsAdd (path, verbose):
		return False

	for top, dirs, files in os.walk(path):
		#basename = os.path.basename (top)
		#if basename == '.svn' or basename == 'CVS':
		if canOmit (top):
			continue

		if not cvsAdd (top, verbose):
			return False
		for f in files:
			#if f == '.svn' or f == 'CVS':
			#	continue
			if canOmit (f):
				continue
			if not cvsAdd (os.path.join(top, f), verbose):
				return False
	
	return True

def cvsAddParents (path, verbose = False):
	#if os.path.basename (path) == '.svn':
	#	return True
	if canOmit (path):
		return True

	if cvsAdd (path, verbose):
		return True

	(head, tail) = os.path.split (path)

	ret = cvsAddParents (head, verbose)

	if not ret:
		return False

	return cvsAdd (path)

# Executes CVS 'remove' command
def cvsRemove (path, verbose = False):
	cvscmd = 'cvs remove -fR %s' % escape (path)

	if execute (cvscmd, verbose):
		return False

	print '[Removed] %s.' % path

	return True

# Executes SVN 'checkout' command
def svnCheckout (svnrepos, rev, verbose = False):
	svncmd = 'svn checkout --non-interactive -r%d %s .' % (rev, escape (svnrepos))

	if execute (svncmd, False, verbose = False):
		return False

	return True

# Executes SVN 'export' command
def svnExport (svnrepos, rev, verbose = False):
	svncmd = 'svn export --force --non-interactive -r%d %s .' % (rev, escape (svnrepos))

	if execute (svncmd, False):
		return False

	return True

def getCurRev ():
	logfile = os.path.join ('.', '.svn2cvs')

	currev = 0
	if os.path.exists (logfile):
		if (os.path.isfile (logfile)):
			fp = open(logfile)
			currev = int (fp.readline ())
			fp.close ()
		else:
			print 'Error: %s is not a regular file.' % logfile
			return -1
	else:
		if execute ('echo 0 > %s' % logfile):
			return -1
		if not cvsAdd (logfile):
			return -1
		if not cvsCommit (logfile, '.svn2cvs created.'):
			return -1

	return currev

def updateCurRev (rev):
	logfile = os.path.join ('.', '.svn2cvs')

	fp = open (logfile, "w")
	fp.write ('%s' % rev)
	fp.close ()

	print '[Updated] .svn2cvs (to revision %d).' % rev


# Converts svn repos to cvs from specified revision.
def convert (svnrepos, revlog, currev):
	count = 0
	for revEntry in revlog:
		count = count+1

		rev = int(revEntry['revision'])
		if rev < currev:
			print 'Warning: Skipped revisoin %d.' % rev
			continue

		print 
		print 'Revision %d [%.2f%%]' % (rev, 100.0*count/len(revlog))
		print

		if not svnExport (svnrepos, rev):
			print 'Error: Failed to checkout revision %d.' % currev
			return False

		ret = True
		for path in revEntry['paths'] :
			action = path['action']
			if action == 'A':
				if path.has_key('copyfrom-path') or path.has_key('copyfrom-rev'):
					ret = cvsAddRecursively (path['value'])
				else:
					ret = cvsAddParents (path['value'])
					#ret = cvsAdd (path['value'])
					#if not ret:
					#	ret = cvsAddP (path['value'])
			elif action == 'M':
				pass
			elif action == 'D':
				ret = cvsRemove (path['value'])
			else:
				print 'Warning: Unrecognized action "%s".' % action
				continue

			if not ret:
				print 'Warning: previous command failed.'
				return False

		msg = 'svn-revision: %d\nauthor: %s\ndate: %s\nmessage: %s' % (rev, revEntry['author'], revEntry['date'], revEntry['msg'])
		if not cvsCommit ('.', msg):
			return False

		updateCurRev (rev)
		if not cvsCommit ('.svn2cvs', 'Revision %d converted.' % rev):
			return False

	return True


def help () :
	print "$Revision: 1.6 $"
	print ("%s [-v|--verbose] [-h|--help] [-d cvsroot] svnrepos cvsmodule" % os.path.basename (sys.argv[0]))
	#print ("%s [-v|--verbose] [-h|--help] [-d cvsroot] [--cvsuser cvsuser] [--cvspasswd cvspasswd] svnrepos cvsmodule" % os.path.basename (sys.argv[0]))


def main () :

	opts, args = getopt.getopt(sys.argv[1:], 'vd:h', 
			['verbose=', 'help', 'cvspasswd=', 'cvsuser='])

	cvsuser = 'anonymous'
	cvspasswd = ''
	cvsmodule = ''
	cvsroot = ''
	for a, b in opts :
		if a in ('-h', '--help') :
			help ()
			sys.exit (0)
		elif a == '--cvsuser' :
			cvsuser = b
		elif a == '--cvspasswd' :
			cvspasswd = b
		elif a == '-d' :
			cvsroot = b
		elif a in ('-v', 'verbose'):
			_verbose = True
	
	if len (args) != 2 :
		help ()
		sys.exit (0)

	(svnrepos, cvsmodule) = args

	# parse revision
	revlog = parseRevision (svnrepos)

	if not revlog:
		sys.exit (1)

	olddir = os.path.realpath (os.curdir)
	localdir = '/var/tmp/svn2cvs-%s' % time.time()

	os.mkdir (localdir)
	os.chdir (localdir)

	if not cvsCheckout (localdir, cvsroot, cvsmodule):
		sys.exit (1)

	currev = getCurRev ()

	print
	print

	if not convert (svnrepos, revlog, currev + 1):
		sys.exit (1)

	print
	print "All %d revisions converted successfully." % getCurRev ()
	print

	print "Cleaning temporary directory."

	execute ('rm -rf %s' % localdir);

	print

	print 'Done.'
	

if __name__ == '__main__' :

	reload (sys)

	sys.setdefaultencoding ('utf-8')

	main ()

